/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.util.osgi;

import java.util.SortedMap;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;

/**
 * FlexibleServiceTracker : The flexible service tracker.
 * 
 * <p>
 * Standard {@link ServiceTracker}s rely on a {@link BundleContext} to find
 * services. Instead, a {@link FlexibleServiceTracker} relies on a
 * {@link Bundle} and handles the bundle life cycle. This can be usefull for
 * classes which do not have access to the bundle context.
 * </p>
 * 
 * @param <S>
 *            The service reference type
 * @param <T>
 *            The tracked service type
 * 
 * @see ServiceTracker
 * @ThreadSafe
 * 
 * @author Romain DUBOIS
 */
public class FlexibleServiceTracker<S, T> {

    /** The service reference type */
    private final Class<S> mType;
    /** The service reference type (String form) */
    private final String mStringType;
    /** The tracking filter */
    private final Filter mFilter;
    /** The bundle used for tracking operations */
    private final Bundle mBundle;

    /** If this tracker is open */
    private boolean mOpen;
    /** The underlying service tracker */
    private ServiceTracker<S, T> mTracker;
    /** The tracking count */
    private int mTrackingOffset = 0;

    /**
     * Constructor.
     * 
     * @param pBundle
     *            The bundle to use for tracking operations
     * @param pType
     *            The service reference type
     */
    public FlexibleServiceTracker(final Bundle pBundle, final Class<S> pType) {
        this(pBundle, pType, null, null);
    }

    /**
     * Constructor.
     * 
     * @param pBundle
     *            The bundle to use for tracking operations
     * @param pType
     *            The service reference type
     */
    public FlexibleServiceTracker(final Bundle pBundle, final String pType) {
        this(pBundle, null, pType, null);
    }

    /**
     * Constructor
     * 
     * @param pBundle
     *            The bundle to use for tracking operations
     * @param pFilter
     *            The tracking filter
     */
    public FlexibleServiceTracker(final Bundle pBundle, final Filter pFilter) {
        this(pBundle, null, null, pFilter);
    }

    /**
     * Inner constructor.
     * 
     * @param pBundle
     *            The bundle to use for tracking operations
     * @param pType
     *            The service reference type
     * @param pStringType
     *            The service reference type (String form)
     * @param pFilter
     *            The tracking filter
     */
    private FlexibleServiceTracker(final Bundle pBundle, final Class<S> pType,
            final String pStringType, final Filter pFilter) {
        super();
        mBundle = pBundle;
        mType = pType;
        mStringType = pStringType;
        mFilter = pFilter;
        mOpen = false;
    }

    /**
     * Close the tracker.
     * 
     * @see ServiceTracker#close()
     */
    public synchronized void close() {
        mOpen = false;
        if (mTracker != null) {
            if (mBundle.getBundleContext() != null) {
                mTracker.close();
            }
            mTracker = null;
        }
    }

    /**
     * Get the first tracked service.
     * 
     * @return The first tracked service, or {@code null} if no service matches
     * @see ServiceTracker#getService()
     */
    public synchronized T getService() {
        if (check()) {
            return mTracker.getService();
        }
        return null;
    }

    /**
     * Get the tracked service associated to the provided reference.
     * 
     * @param pReference
     *            The reference of the tracked service to return
     * @return The tracked service
     * @see ServiceTracker#getService(ServiceReference)
     */
    public synchronized T getService(final ServiceReference<S> pReference) {
        if (check()) {
            return mTracker.getService(pReference);
        }
        return null;
    }

    /**
     * Get the first tracked service reference.
     * 
     * @return The first tracked services reference
     * @see ServiceTracker#getServiceReference()
     */
    public synchronized ServiceReference<S> getServiceReference() {
        if (check()) {
            return mTracker.getServiceReference();
        }
        return null;
    }

    /**
     * Get the tracked service references.
     * 
     * @return The tracked services references
     * @see ServiceTracker#getServiceReferences()
     */
    public synchronized ServiceReference<S>[] getServiceReferences() {
        if (check()) {
            return mTracker.getServiceReferences();
        }
        return null;
    }

    /**
     * Get the tracked services.
     * 
     * @return The tracked services
     * @see ServiceTracker#getServices()
     */
    public synchronized Object[] getServices() {
        if (check()) {
            return mTracker.getServices();
        }
        return null;
    }

    /**
     * Get the tracked services.
     * 
     * @param pArray
     *            The array to fill or to use to create the returned array
     * @return The tracked services
     * @see ServiceTracker#getServices(Object[])
     */
    public synchronized T[] getServices(final T[] pArray) {
        if (check()) {
            return mTracker.getServices(pArray);
        }
        return null;
    }

    /**
     * Get tracking map.
     * 
     * @return The tracking map
     * @see ServiceTracker#getTracked()
     */
    public synchronized SortedMap<ServiceReference<S>, T> getTracked() {
        if (check()) {
            return mTracker.getTracked();
        }
        return null;
    }

    /**
     * Get the current tracking count.
     * 
     * @return The current tracking count
     * @see ServiceTracker#getTrackingCount()
     */
    public synchronized int getTrackingCount() {
        if (check()) {
            return mTracker.getTrackingCount() + mTrackingOffset;
        }
        return mTrackingOffset;
    }

    /**
     * Test if at least one matching service is tracked.
     * 
     * @return {@code true} if no matching service has been found, {@code false}
     *         otherwise
     * @see ServiceTracker#isEmpty()
     */
    public synchronized boolean isEmpty() {
        if (check()) {
            return mTracker.isEmpty();
        }
        return true;
    }

    /**
     * Open the tracker.
     * 
     * @see ServiceTracker#open()
     */
    public synchronized void open() {
        mOpen = true;
        check();
    }

    /**
     * Get the number of tracked services.
     * 
     * @return The number of tracked services
     * @see ServiceTracker#size()
     */
    public synchronized int size() {
        if (check()) {
            return mTracker.size();
        }
        return 0;
    }

    /**
     * Get the first tracked service and wait for it if needed.
     * 
     * @param pTimeout
     *            The waiting timeout
     * @return The first tracked service or {@code null} if no service matches
     *         before timeout expires
     * @throws InterruptedException
     *             If current thread has been interrupted
     * @see ServiceTracker#waitForService(long)
     */
    public synchronized T waitForService(final long pTimeout) throws InterruptedException {
        if (check()) {
            return mTracker.waitForService(pTimeout);
        }

        // Wait 'manually'
        wait(pTimeout);
        return getService();
    }

    /**
     * Check the tracker state.
     * 
     * @return If the tracker is valid
     */
    private boolean check() {
        if (mOpen && (mTracker == null || mTracker.size() == 0)) {
            final BundleContext lContext = mBundle.getBundleContext();

            // Create tracker
            if (mTracker == null && mBundle.getBundleContext() != null) {
                if (mType != null) {
                    mTracker = new ServiceTracker<S, T>(lContext, mType, null);
                } else if (mStringType != null) {
                    mTracker = new ServiceTracker<S, T>(lContext, mStringType, null);
                } else {
                    mTracker = new ServiceTracker<S, T>(lContext, mFilter, null);
                }
                mTracker.open();
            }

            // Close tracker
            else if (mTracker != null && mBundle.getBundleContext() == null) {
                mTrackingOffset += mTracker.getTrackingCount();
                mTracker.close();
                mTracker = null;
            }
        }

        return mTracker != null;
    }
}
